package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"
)

type User struct {
	ID               int           `json:"id"`
	Name             string        `json:"name"`
	Username         string        `json:"username"`
	State            string        `json:"state"`
	AvatarURL        string        `json:"avatar_url"`
	WebURL           string        `json:"web_url"`
	CreatedAt        time.Time     `json:"created_at"`
	Bio              interface{}   `json:"bio"`
	Location         interface{}   `json:"location"`
	Skype            string        `json:"skype"`
	Linkedin         string        `json:"linkedin"`
	Twitter          string        `json:"twitter"`
	WebsiteURL       string        `json:"website_url"`
	Organization     interface{}   `json:"organization"`
	LastSignInAt     time.Time     `json:"last_sign_in_at"`
	ConfirmedAt      time.Time     `json:"confirmed_at"`
	LastActivityOn   string        `json:"last_activity_on"`
	Email            string        `json:"email"`
	ThemeID          int           `json:"theme_id"`
	ColorSchemeID    int           `json:"color_scheme_id"`
	ProjectsLimit    int           `json:"projects_limit"`
	CurrentSignInAt  time.Time     `json:"current_sign_in_at"`
	Identities       []interface{} `json:"identities"`
	CanCreateGroup   bool          `json:"can_create_group"`
	CanCreateProject bool          `json:"can_create_project"`
	TwoFactorEnabled bool          `json:"two_factor_enabled"`
	External         bool          `json:"external"`
	PrivateProfile   interface{}   `json:"private_profile"`
	IsAdmin          bool          `json:"is_admin"`
}

type Group struct {
	ID                   int         `json:"id"`
	WebURL               string      `json:"web_url"`
	Name                 string      `json:"name"`
	Path                 string      `json:"path"`
	Description          string      `json:"description"`
	Visibility           string      `json:"visibility"`
	LfsEnabled           bool        `json:"lfs_enabled"`
	AvatarURL            string `json:"avatar_url"`
	RequestAccessEnabled bool        `json:"request_access_enabled"`
	FullName             string      `json:"full_name"`
	FullPath             string      `json:"full_path"`
	ParentID             int `json:"parent_id"`
}

type StatusCodeError struct {
	StatusCode int
	ErrorString string
}
func(s *StatusCodeError) Error() string {
	return fmt.Sprintf("status wrong: %d[%s]", s.StatusCode, s.ErrorString)
}

func createStatusCodeErrorWithErrorString(s int, es string) *StatusCodeError{
	return &StatusCodeError{
		StatusCode: s,
		ErrorString: es,
	}
}

func createStatusCodeError(s int) *StatusCodeError{
	return &StatusCodeError{
		StatusCode: s,
		ErrorString: "",
	}
}

type CallFunc func(req *http.Request)

func EmptyCallFunc(req *http.Request) {

}

func requestGitlabWithCallback(api string, method string, input io.Reader, cf CallFunc) (ret []byte, err error) {
	url := fmt.Sprintf("%s/api/v4/%s", apiDomain, api)

	client := &http.Client {
	}
	req, err := http.NewRequest(method, url, input)

	if err != nil {
		fmt.Println(err)
		return
	}
	req.Header.Add("Private-Token", token)
	cf(req)

	res, err := client.Do(req)
	if err != nil {
		return
	}
	defer res.Body.Close()

	if !(res.StatusCode >= 200 && res.StatusCode < 300) {
		errBody, rerr := ioutil.ReadAll(res.Body)
		var errString string
		if rerr == nil {
			errString = string(errBody)
		}
		err = createStatusCodeErrorWithErrorString(res.StatusCode, errString)
		return
	}
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return
	}
	ret = body
	return
}

func requestGitlab(api string, method string, input io.Reader) (ret []byte, err error) {
	return requestGitlabWithCallback(api, method, input, EmptyCallFunc)
}


func requestUsers() (users []*User, err error) {
	ret, err := requestGitlab("users", http.MethodGet, nil)
	if err != nil {
		return
	}
	err = json.Unmarshal(ret, &users)
	if err != nil {
		return
	}
	return
}

const CourseAdminGroupName string = "course-admin"

func requestAdminGroup()(group *Group, err error) {
	return requestGroup(CourseAdminGroupName, false)
}

func requestGroup(name string, needParent bool) (g *Group,err error) {
	qc := name
	if needParent {
		qc = CourseAdminGroupName + "%2F" + name
	}
	ret, err := requestGitlab(fmt.Sprintf("groups/%s", qc), http.MethodGet, nil)
	if err != nil {
	    return
	}
	err = json.Unmarshal(ret, &g)
	if err != nil {
		return
	}
	return
}

func createAdminGroup()(group *Group, err error) {
	group = &Group{
		Name: CourseAdminGroupName,
		Path: CourseAdminGroupName,
		Visibility: "private",
	}
	err = createGroup(group)
	return
}

func createGroup(group *Group) (err error) {
	values := url.Values{}
	values.Set("name", group.Name)
	values.Set("path", group.Path)
	values.Set("visibility", group.Visibility)
	if group.ParentID > 0 {
		values.Set("parent_id", strconv.Itoa(group.ParentID))
	}
	_, err = requestGitlab(fmt.Sprintf("groups?%s", values.Encode()), http.MethodPost, nil)
	if err != nil {
	    return
	}
	return
}

func getGroupMembers(group *Group)(users []*User, err error) {
	qc := group.Name
	if group.ID != 0 {
		qc = strconv.Itoa(group.ID)
	} else if group.Path != "" {
		qc = group.Path
	}
	ret, err := requestGitlab(fmt.Sprintf("groups/%s/members", qc), http.MethodGet, nil)
	if err != nil {
		return
	}
	err = json.Unmarshal(ret, &users)
	if err != nil {
		return
	}
	return
}

func getNameFromEmail(email string) string {
	return strings.Split(email, "@")[0]
}

const PassSuffix string = "sist2021"

func getUser(name, email string)(u *User, err error) {
	ret, err := requestGitlab(fmt.Sprintf("users?username=%s", name), http.MethodGet, nil)
	if err != nil {
		return
	}
	var users []*User
	err = json.Unmarshal(ret, &users)
	if err != nil {
		return
	}
	if len(users) == 0 {
		err = createStatusCodeError(http.StatusNotFound)
		return
	}
	for _, user := range users {
		if user.Email == email {
			return user, nil
		}
	}
	err = createStatusCodeError(http.StatusNotFound)
	return
}

func createUser(u *User)(err error) {
	return createUserWithPass(u, u.Username + PassSuffix)
}

const char = "abcdefghijklmnopqrstuvwxyz0123456789"

func randChar(size int) string {
	rand.NewSource(time.Now().UnixNano()) // 产生随机种子
	var s bytes.Buffer
	for i := 0; i < size; i ++ {
		s.WriteByte(char[rand.Int63() % int64(len(char))])
	}
	return s.String()
}

func createUserWithPass(u *User, p string)(err error) {
	values := url.Values{}
	values.Set("skip_confirmation", "true")
	if u.IsAdmin {
		values.Set("admin", "true")
	}
	values.Set("email", u.Email)
	values.Set("password", p)
	values.Set("username", u.Username)
	values.Set("name", u.Name)
	_, err = requestGitlab(fmt.Sprintf("users?%s", values.Encode()), http.MethodPost, nil)
	return
}
func deleteProtectedBranch(p *Project, branch string)(err error) {
	_, err = requestGitlab(fmt.Sprintf("projects/%d/protected_branches/%s", p.ID, branch), http.MethodDelete, nil)
	_, err = requestGitlab(fmt.Sprintf("projects/%d/protected_branches?name=%s&push_access_level=30&merge_access_level=30", p.ID, branch), http.MethodPost, nil)
	return
}
func addMember(g *Group, u *User)(err error) {
	values := url.Values{}
	values.Set("user_id", strconv.Itoa(u.ID))
	accessLevel := "30"
	if u.IsAdmin  {
		accessLevel = "40"
	}
	values.Set("access_level", accessLevel)
	vs := values.Encode()
	qc := g.Name
	if g.ID != 0 {
		qc = strconv.Itoa(g.ID)
	} else if g.Path != "" {
		qc = g.Path
	}
	_, err = requestGitlabWithCallback(fmt.Sprintf("/groups/%s/members", qc), http.MethodPost,  bytes.NewReader([]byte(vs)), func(req *http.Request) {
		//req.Header.Set("Content-Type", "application/octet-stream")
	})
	return
}

func removeMember(g *Group, u *User)(err error) {
	if u.Name == "Administrator" || u.ID == 1 {
		return
	}
	qc := g.Name
	if g.ID != 0 {
		qc = strconv.Itoa(g.ID)
	} else if g.Path != "" {
		qc = g.Path
	}
	_, err = requestGitlab(fmt.Sprintf("/groups/%s/members/%d", qc, u.ID), http.MethodDelete,  nil)
	return
}

func modifyUserAdmin(user *User, isAdmin bool) (err error) {
	admin := "false"
	if isAdmin {
		admin = "true"
	}
	values := url.Values{}
	values.Set("admin", admin)
	err = modifyUser(user, &values)
	return
}

func modifyUser(user *User, values *url.Values) (err error) {
	_, err = requestGitlab(fmt.Sprintf("users/%d?%s", user.ID, values.Encode()), http.MethodPut, nil)
	return
}
type Project struct {
	ID                int           `json:"id"`
	Description       interface{}   `json:"description"`
	Name              string        `json:"name"`
	NameWithNamespace string        `json:"name_with_namespace"`
	Path              string        `json:"path"`
	PathWithNamespace string        `json:"path_with_namespace"`
	CreatedAt         time.Time     `json:"created_at"`
	DefaultBranch     interface{}   `json:"default_branch"`
	TagList           []interface{} `json:"tag_list"`
	SSHURLToRepo      string        `json:"ssh_url_to_repo"`
	HTTPURLToRepo     string        `json:"http_url_to_repo"`
	WebURL            string        `json:"web_url"`
	ReadmeURL         interface{}   `json:"readme_url"`
	AvatarURL         interface{}   `json:"avatar_url"`
	StarCount         int           `json:"star_count"`
	ForksCount        int           `json:"forks_count"`
	LastActivityAt    time.Time     `json:"last_activity_at"`
	Namespace         struct {
		ID       int         `json:"id"`
		Name     string      `json:"name"`
		Path     string      `json:"path"`
		Kind     string      `json:"kind"`
		FullPath string      `json:"full_path"`
		ParentID interface{} `json:"parent_id"`
	} `json:"namespace"`
	Links struct {
		Self          string `json:"self"`
		Issues        string `json:"issues"`
		MergeRequests string `json:"merge_requests"`
		RepoBranches  string `json:"repo_branches"`
		Labels        string `json:"labels"`
		Events        string `json:"events"`
		Members       string `json:"members"`
	} `json:"_links"`
	Archived   bool   `json:"archived"`
	Visibility string `json:"visibility"`
	Owner      struct {
		ID        int    `json:"id"`
		Name      string `json:"name"`
		Username  string `json:"username"`
		State     string `json:"state"`
		AvatarURL string `json:"avatar_url"`
		WebURL    string `json:"web_url"`
	} `json:"owner"`
	ResolveOutdatedDiffDiscussions            bool          `json:"resolve_outdated_diff_discussions"`
	ContainerRegistryEnabled                  bool          `json:"container_registry_enabled"`
	IssuesEnabled                             bool          `json:"issues_enabled"`
	MergeRequestsEnabled                      bool          `json:"merge_requests_enabled"`
	WikiEnabled                               bool          `json:"wiki_enabled"`
	JobsEnabled                               bool          `json:"jobs_enabled"`
	SnippetsEnabled                           bool          `json:"snippets_enabled"`
	SharedRunnersEnabled                      bool          `json:"shared_runners_enabled"`
	LfsEnabled                                bool          `json:"lfs_enabled"`
	CreatorID                                 int           `json:"creator_id"`
	ImportStatus                              string        `json:"import_status"`
	ImportError                               interface{}   `json:"import_error"`
	OpenIssuesCount                           int           `json:"open_issues_count"`
	RunnersToken                              string        `json:"runners_token"`
	PublicJobs                                bool          `json:"public_jobs"`
	CiConfigPath                              interface{}   `json:"ci_config_path"`
	SharedWithGroups                          []interface{} `json:"shared_with_groups"`
	OnlyAllowMergeIfPipelineSucceeds          bool          `json:"only_allow_merge_if_pipeline_succeeds"`
	RequestAccessEnabled                      bool          `json:"request_access_enabled"`
	OnlyAllowMergeIfAllDiscussionsAreResolved bool          `json:"only_allow_merge_if_all_discussions_are_resolved"`
	PrintingMergeRequestLinkEnabled           bool          `json:"printing_merge_request_link_enabled"`
	MergeMethod                               string        `json:"merge_method"`
}

const RepoSuffix = "-repo"

func getRepoName(n string) string {
	return n + RepoSuffix
}

func getRepo(name string)(p *Project, err error) {
	p = new(Project)
	ret, err := requestGitlab(fmt.Sprintf("projects/%s%s%s", name, "%2F",getRepoName(name)), http.MethodGet, nil)
	if err != nil {
	    return
	}
	err = json.Unmarshal(ret, p)
	if err != nil {
		return
	}
	return
}

func createRepo(name string, userId int)(p *Project, err error) {
	values := url.Values{}
	values.Set("name", getRepoName(name))
	values.Set("jobs_enabled", "false")
	values.Set("visibility", "private")
	ret, err := requestGitlab(fmt.Sprintf("projects/user/%d", userId), http.MethodPost, bytes.NewReader([]byte(values.Encode())))
	if err != nil {
		return
	}
	p = new(Project)
	err = json.Unmarshal(ret, p)
	if err != nil {
		return
	}
	return
}

func shareProject(p *Project, g *Group, groupAccess string) (err error) {
	values := url.Values{}
	values.Set("group_id", strconv.Itoa(g.ID))
	values.Set("group_access", groupAccess)
	_, err = requestGitlab(fmt.Sprintf("projects/%d/share", p.ID), http.MethodPost, bytes.NewReader([]byte(values.Encode())))
	serr, ok := err.(*StatusCodeError)
	if ok && serr.StatusCode == http.StatusConflict {
		err = nil
	}
	return
}